package piwan;

import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;

/** A Camera object provides functionality for a RTS-like camera, having intuitive methods for moving the camera around and also providing an EventHandler for
 * user-controlled cameras. It provides two view-modes, perpendicular and non-perpendicular which can be chosen with the method setPerpendicular().
 * Before rendering 3D objects one needs to call the loadMatrices() method, that updates the view to the camera viewpoint.
 * 
 * @author Cristian Cobzarenco
 *
 */
public class Camera {
	private static final double zoomMin = 0.07;
	private static final double zoomMax = 0.15;
	private static final double zoomNonPerpMult = 1.8;
	private static final double zoomWheelMult = 1.1;
	private static final double scrollSpeed = 0.8;
	private static final double scrollBorderWidth = 5;
	private static final Vector2 eyeNonPerp = (new Vector2(0.1,0.5)).unit();
	private static final double xlimit = 6.0, ylimit = 3.0; 
	
	private class Handler extends EventHandler implements FrameListener {
		public boolean onKeyPressed(KeyEvent e) {
			if( e.getKeyChar() == 'c' ) {
				setPerpendicular(!perpCam);
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_W) {
				scroll.y = -scrollSpeed/zoom;
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_A) {
				scroll.x = -scrollSpeed/zoom;
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_S) {
				scroll.y = scrollSpeed/zoom;
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_D) {
				scroll.x = scrollSpeed/zoom;
				return true;
			}
				
			return false;
		}
		
		public boolean onKeyReleased(KeyEvent e) {
			if(e.getKeyCode() == KeyEvent.VK_W) {
				scroll.y = 0.0;
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_A) {
				scroll.x = 0.0;
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_S) {
				scroll.y = 0.0;
				return true;
			} else if( e.getKeyCode() == KeyEvent.VK_D) {
				scroll.x = 0.0;
				return true;
			}
			return false;
		}
		
		public boolean onMouseMotion(MouseEvent e) {
			if( e.getX() < scrollBorderWidth )
				scroll.x = -scrollSpeed/zoom;
			else if( e.getX() > gui.getWidth() - scrollBorderWidth )
				scroll.x = scrollSpeed/zoom;
			else
				scroll.x = 0.0;
			
			if( e.getY() > gui.getHeight() - scrollBorderWidth )
				scroll.y = scrollSpeed/zoom; 
			else if( e.getY() < scrollBorderWidth )
				scroll.y = -scrollSpeed/zoom;
			else
				scroll.y = 0.0;
			
			wmouse.set(getMouseRay(e.getX(), e.getY()).intersectXZ());
			return false;
		}
		
		public boolean onMouseWheel(MouseWheelEvent e) {
			if(  e.getWheelRotation()>0 ) zoomOut();
			else zoomIn();
			return true;
		}
		
		public void newFrame( double delta ) {
			if(scroll.x!=0.0 || scroll.y != 0.0) {
				if(perpCam) 
					moveWith(scroll.times(delta));
				else {
					Vector2 vs = new Vector2(eye.x*scroll.y+eye.y*scroll.x,eye.y*scroll.y-eye.x*scroll.x);
					vs.mul(delta);
					moveWith( vs );
				}
			}
		}
	}
	
	/** Creates a new camera and adds the handler to the MainWindow.
	 * 
	 * @param igui MainWindow to add the handler to
	 */
	public Camera(MainWindow igui) {
		gui = igui;
		gl = gui.getGL();
		glu = gui.getGLU();
		hdl = new Handler();
		setAspectRatio();
		addHandler();
	}
	
	/** Gets the zoom level of the camera. A higher zoom means more zoomed-in.
	 * 
	 * @return zoom level
	 */
	public double getZoom() { return zoom; }
	
	/** In case of a user-controlled camera this returns the projection of the mouse cursor on the XZ plane.
	 * 
	 * @return the point on the XZ plane
	 */
	public Vector2 getWorldMouse() {
		return wmouse;
	}
	
	/** Re-adds the handler to the stored MainWindow.
	 * 
	 */
	public void addHandler() {
		gui.addFrameListener(hdl);
		gui.addEventHandler(hdl);
	}
	
	/** Removes the handler for user-controlled handlers.
	 * 
	 */
	public void removeHandler() {
		gui.removeFrameListener(hdl);
		gui.removeEventHandler(hdl);
	}
	
	/** Gets a ray starting from the (x,y) point on the screen in the direction of the camera.
	 * 
	 * @param x the x-coordinate of the point
	 * @param y the y-coordinate of the point
	 * @return
	 */
	public Ray getMouseRay( int x, int y ) {
		double clickX = (double)x;
		double clickY = (double)(gui.getHeight()-y);
		double[] vray = new double[3];
		Vector3 p1,p2;
		glu.gluUnProject(clickX,clickY,0.0,matModel,0,matProj,0,gui.getViewport(),0,vray,0);
		p1 = new Vector3(vray);
		glu.gluUnProject(clickX,clickY,1.0,matModel,0,matProj,0,gui.getViewport(),0,vray,0);
		p2 = new Vector3(vray);
		
		return new Ray(p1,p2);
	}
	
	/** Sets the position of the camera (or more precisely of the point the camera is looking at).
	 * 
	 * @param newPos the new position of the camera
	 */
	public void setPosition( Vector2 newPos ) {
		pos.set(newPos);
		mvNeedsUpdate = true;
	}
	
	/** Moves the camera relative to its current position.
	 * 
	 * @param with the displacement to move the camera with
	 */
	public void moveWith( Vector2 with ) {
		pos.add(with);
		mvNeedsUpdate = true;
	}
	
	/** Sets the zoom level - higher value means more zoomed-in.
	 * 
	 * @param newZoom the new zoom level
	 */
	public void setZoom( double newZoom ) {
		if(newZoom<zoomMin) newZoom = zoomMin;
		else if(newZoom>zoomMax) newZoom = zoomMax;
		zoom = newZoom;
		mvNeedsUpdate = true;
	}
	
	/** Zooms in by one step (this corresponds to the wheel moving one unit). */
	public void zoomIn() { setZoom(zoom*zoomWheelMult); }
	
	/** Zooms out by one step (this corresponds to the wheel moving one unit). */
	public void zoomOut() { setZoom(zoom/zoomWheelMult); }
	
	/** Sets the view mode to either perpendicular or non-perpendicular. */
	public void setPerpendicular(boolean b) {
		perpCam = b;
		mvNeedsUpdate = true;
	}
	
	/** Updates the aspect ratio based on the size of the stored MainWindow. */
	public void setAspectRatio() {
		setAspectRatio( (double)gui.getWidth()/gui.getHeight() );
	}
	
	/** Sets a new aspect ratio.
	 * 
	 * @param newAspect the new aspect ratio to be used
	 */
	public void setAspectRatio( double newAspect ) {
		aspect = newAspect;
		prNeedsUpdate = true;
	}
	
	/** Loads the matrices to set the current view. To be called before 3D rendering. */
	public void loadMatrices() {
		if(prNeedsUpdate) { updateProj(); prNeedsUpdate = false; }
		if(mvNeedsUpdate) { updateModel(); mvNeedsUpdate = false; }
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glLoadMatrixd(matProj, 0);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glLoadMatrixd(matModel, 0);
	}
	
	private void updateProj() {
		gl.glPushMatrix();
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(45.0, aspect, 0.1, 20.0);
		gl.glGetDoublev(GL.GL_PROJECTION_MATRIX, matProj, 0);
		gl.glPopMatrix();
	}
	
	private void updateModel() {
		gl.glPushMatrix();
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		if(pos.x<-xlimit) pos.x = -xlimit;
		else if(pos.x>xlimit) pos.x = xlimit;
		if(pos.y<-ylimit) pos.y = -ylimit;
		else if(pos.y>ylimit) pos.y = ylimit;
		if(perpCam)
			glu.gluLookAt(pos.x, 1.0/zoom, pos.y, pos.x, 0.0, pos.y, 0.0, 0.0, -1.0 );
		else
			glu.gluLookAt(pos.x+eye.x/zoom/zoomNonPerpMult, 1.0/zoom/zoomNonPerpMult, pos.y + eye.y/zoom/zoomNonPerpMult, pos.x, 0.0, pos.y, 0.0, 1.0, 0.0 );
		gl.glGetDoublev(GL.GL_MODELVIEW_MATRIX, matModel, 0);
		gl.glPopMatrix();
	}
	
	private MainWindow		gui=null;
	private GL			gl=null;
	private GLU 		glu=null;
	private Handler		hdl=null;
	private Vector2		pos=new Vector2(0.0,0.0);
	private double[] 	matProj = new double[16];
	private double[]	matModel = new double[16];
	private boolean		perpCam=false;
	private Vector2		eye = eyeNonPerp;
	private double		zoom = (zoomMax+zoomMin)/2.0;
	private double		aspect = 1.0;
	private Vector2		scroll = new Vector2(0.0,0.0);
	private boolean		mvNeedsUpdate=true;
	private boolean		prNeedsUpdate=true;
	private Vector2		wmouse = new Vector2(0.0,0.0);
	
	
	
}
